Contents
Usage
ProMotion::TableScreen allows you to easily create lists or "tables" as iOS calls them. It's a subclass of UITableViewController and has all the goodness of PM::Screen with some additional magic to make the tables work beautifully.
Table Screens | Grouped Tables | Searchable | Refreshable |
---|---|---|---|
class TasksScreen < PM::TableScreen
title "Tasks"
refreshable
searchable placeholder: "Search tasks", no_results: "Sorry, Try Again!"
row_height :auto, estimated: 44
def on_load
@tasks = []
load_async
end
def table_data
[{
cells: @tasks.map do |task|
{
title: task.title,
subtitle: task.description,
action: :edit_task,
arguments: { task: task }
}
end
}]
end
def on_refresh
load_async
end
def load_async
# Assuming we're loading tasks from some cloud service
Task.async_load do |tasks|
@tasks = tasks
stop_refreshing
update_table_data
end
end
end
Example of a PM::GroupedTableScreen
: https://gist.github.com/jamonholmgren/382a6cf9963c5f0b2248
Methods
table_data
Method that is called to get the table's cell data and build the table.
It consists of an array of cell sections, each of which contain an array of cells.
def table_data
[{
title: "Northwest States",
cells: [
{ title: "Oregon", action: :visit_state, arguments: { state: @oregon }},
{ title: "Washington", action: :visit_state, arguments: { state: @washington }}
]
}]
end
You'll often be iterating through a group of objects. You can use .map
to easily build your table:
def table_data
[{
title: "States",
cells:
State.all.map do |state|
{
title: state.name,
action: :visit_state,
arguments: { state: state }
}
end
}]
end
def visit_state(args={})
mp args[:state] # => instance of State
end
View the Reference: All available table_data options for an example with all available options.
Accessory Views
TableScreen
supports the :switch
accessory and custom accessory views.
Using Switches:
{
title: "Switch With Action",
accessory: {
view: :switch,
value: true, # switched on
action: :foo
}
}, {
title: "Switch with Action and Parameters",
accessory: {
view: :switch,
action: :foo,
arguments: { bar: 12 }
}
}, {
title: "Switch with Cell Tap, Switch Action and Parameters",
accessory: {
view: :switch,
action: :foo,
arguments: { bar: 3 },
},
action: :fizz,
arguments: { buzz: 10 }
}
Using a custom accessory
view:
button1 = set_attributes UIButton.buttonWithType(UIButtonTypeRoundedRect), {
"setTitle:forState:" => [ "A", UIControlStateNormal ]
}
button2 = set_attributes UIButton.buttonWithType(UIButtonTypeRoundedRect), {
"setTitle:forState:" => [ "B", UIControlStateNormal ]
}
button1.frame = [[ 0, 0 ], [ 20, 20 ]]
button2.frame = [[ 0, 0 ], [ 20, 20 ]]
[{
title: "",
cells: [{
title: "My Cell with custom button",
accessory: { view: button1 }
}, {
title: "My Second Cell with another custom button",
accessory: { view: button2 }
}]
}]
However, adding custom accessory views like this is not recommended unless your use case is very simple. Instead, subclass PM::TableViewCell
and provide setters that create the subviews or accessoryView that you want. You can find a blog post demonstrating how this is done here: http://jamonholmgren.com/creating-a-custom-uitableviewcell-with-promotion
update_table_data
Causes the table data to be refreshed, such as when a remote data source has been downloaded and processed.
class MyTableScreen < PM::TableScreen
def on_load
MyItem.pull_from_server do |items|
@table_data = [{
cells: items.map do |item|
{
title: item.name,
action: :tapped_item,
arguments: { item: item }
}
end
}]
update_table_data
end
end
def table_data
@table_data ||= []
end
def tapped_item(item)
open ItemDetailScreen.new(item: item)
end
end
table_data_index
This method allows you to create a "jumplist", the index on the right side of the table
A good way to do this is to grab the first letter of the title of each section:
def table_data_index
# Returns an array of the first letter of the title of each section.
table_data.collect{ |section| (section[:title] || " ")[0] }
end
on_cell_created(cell, data)
Called when a cell is created (not dequeued). data
is the cell hash you provided in the table_data
method.
It's recommended that you call super
if you override this method.
def on_cell_created(cell, data)
super
cell.my_cool_method(data[:properties][:my_property])
cell.contentView.backgroundColor = UIColor.purpleColor
end
on_cell_reused(cell, data)
Called when a cell is dequeued and re-used. data
is the cell hash you provided in the table_data
method.
It's recommended that you call super
if you override this method.
def on_cell_reused(cell, data)
super
cell.my_cool_method(data[:properties][:my_property])
cell.contentView.backgroundColor = UIColor.purpleColor
end
will_display_cell(cell, index_path)
Fires right before a cell is displayed in a table. Use this method to do additional setup on the cell, or other operations such as infinite scroll.
def will_display_cell(cell, index_path)
cell.backgroundColor = UIColor.clearColor
if index_path.row >= @data.length
load_more_data # infinite scroll
end
end
on_cell_deleted(cell, index_path)
If you specify editing_style: :delete
in your cell, you can swipe to reveal a delete button on that cell. When you tap the button, the cell will be removed in an animated fashion and the cell will be removed from its respective table_data
section.
If you need a callback for every cell that's deleted, you can implement the on_cell_deleted(cell)
method, where cell
is the attributes form the original cell data object. Returning false
will cancel the delete action. Anything else will allow it to proceed.
Example:
def on_cell_deleted(cell, index_path)
if cell[:arguments][:some_value] == "something"
App.alert "Sorry, can't delete that row." # BubbleWrap alert
false
else
RemoteObject.find(cell[:arguments][:id]).delete_remotely
true # return anything *but* false to allow deletion in the UI
end
end
delete_row(indexpath, animation=nil)
You can call delete_row(indexpath, animation)
to delete. Both the UI and the internal
data hash are updated when you do this.
def my_delete_method(section, row)
# the 2nd argument is optional. Defaults to :automatic
delete_row(NSIndexPath.indexPathForRow(row, inSection:section), :fade)
end
table_header_view
You can give the table a custom header view (this is different from a section header view, which is below) by defining:
def table_header_view
# Return a UIView subclass here and it will be set at the top of the table.
end
This is useful for information that needs to only be at the very top of a table.
will_display_header(view)
You can customize the section header views just before they are displayed on the table. This is different from table header view, which is above.
def will_display_header(view)
view.tintColor = UIColor.redColor
view.textLabel.setTextColor(UIColor.blueColor)
end
table_footer_view
You can give the table a custom footer view (this is different from a section footer view) by defining:
def table_footer_view
# Return a UIView subclass here and it will be set at the bottom of the table.
end
This is useful for information that needs to only be at the very bottom of a table.
Class Methods
searchable(placeholder: "placeholder text", no_results: "some short qiup here", with: -> (cell, search_string){})
Class method to make the current table searchable.
class MyTableScreen < PM::TableScreen
searchable placeholder: "Search This Table"
end
Specifying no_results:
will change the text that is displayed when there are
no results found.
class MyTableScreen < PM::TableScreen
searchable placeholder: "Search This Table", no_results: "BZZZZZ! Try Again!"
end
Without a with:
specifier, search is performed on the title
attribute, and
the search_text
attribute, if present. If you want to create a custom search
method, specify it as the value of the with
key (find_by
, search_by
and filter_by
are aliases). E.g.:
class MyTableScreen < PM::TableScreen
searchable placeholder: "Search This Table", with: -> (cell, search_string){
cell[:properties][:some_obscure_attribute].strip.downcase.include? search_string.strip.downcase
}
end
or if you want to create a version that is less resistant to refactoring:
class MyTableScreen < PM::TableScreen
searchable placeholder: "Search This Table", with: :custom_search_method
def custom_search_method(cell, search_string)
cell[:properties][:some_obscure_attribute].strip.downcase.include? search_string.strip.downcase
end
end
To initially hide the search bar behind the nav bar until the user scrolls it into view, use hide_initially
.
class MyTableScreen < PM::TableScreen
searchable hide_initially: true
end
You can prevent any table cell from being included in search results by setting the cell attribute searchable
to false
like this:
[{
title: "This cell will appear in the search",
},{
title: "This cell will not",
searchable: false
}]
You can supply additional textual data that you want to be searchable but not display anywhere on the cell by setting the cell attribute search_text
to a string. Cells with search_text
will display in search results if the search term matches either the title
or the search_text
attributes.
[{
title: "Searchable via Title"
},{
title: "Searchable via Title",
search_text: "and will match these words too!"
}]
If you need to know if the current table screen is being searched, searching?
will return true
if the user has entered into the search bar (even if there is no search results yet).
To get the text that a user has entered into the search bar, you can call search_string
for what the data was actually searched against and original_search_string
to get the actual text the user entered. These methods will return back a String
or a falsey object (nil
or false
).
You can also implement methods in your TableScreen
that are called when the search starts or ends:
def will_begin_search
puts "the user tapped the search bar!"
end
def will_end_search
puts "the user tapped the 'cancel' button!"
end
row_height(height, options = {})
Class method to set the row height for each UITableViewCell. You can use iOS 8's 'automatic' row height feature by passing :auto
as the first argument.
class MyTableScreen < PM::TableScreen
row_height :auto, estimated: 44
end
refreshable(options = {})
Class method to make the current table have pull-to-refresh. All parameters are optional.
If you do not specify a callback, it will assume you've implemented an on_refresh
method in your tableview.
class MyTableScreen < PM::TableScreen
refreshable callback: :on_refresh,
pull_message: "Pull to refresh",
refreshing: "Refreshing data…",
updated_format: "Last updated at %s",
updated_time_format: "%l:%M %p"
def on_refresh
MyItems.pull_from_server do |items|
@my_items = items
end_refreshing
update_table_data
end
end
end
If you initiate a refresh event manually by calling start_refreshing
, the table view will automatically scroll down to reveal the spinner at the top of the table.
indexable
This simply takes the first letter of each of your section titles and uses those for the "jumplist" on the right side of your table screen.
class MyTable < PM::TableScreen
indexable
# ...
end
longpressable
This will allow you to specify an additional "long_press_action" on your table cells.
class MyTable < PM::TableScreen
longpressable
def table_data
[{
cells: [{
title: "Long press cell",
action: :normal_action,
long_press_action: :long_press_action,
arguments: { foo: "Will be sent along with either action as arguments" }
}]
}]
end
end
Accessors
You get all the normal accessors of PM::Screen
, but no documented TableScreen accessors are available.
Moveable cells
You can specify cells to be moveable in each individual cell hash. If you want the cells to only be moveable within their own section, define moveable: :section
in each cell hash.
When you want the user to see the moveable drag handles, call toggle_edit_mode
or edit_mode(enabled:true)
.
Finally, define a method:
def on_cell_moved(args)
# Do something here
end
The argument passed to on_cell_moved
is a hash in the form of:
{
:paths => {
:from => #<NSIndexPath:0xb777380>,
:to => #<NSIndexPath:0xb777390>
},
:cell => {
:title => "Whatever",
:moveable => true
# Your other cell attributes
}
}